using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMovement : MonoBehaviour
{
    public GameObject targetObject;
    public Vector3 targetOffset;
    public Vector2 targetDirections;
    public float targetRotation;
    public float targetDistance = 5;

    public float L0Weight = 32;
    public float L1Weight = 16;

    public bool forceSync = false;


    private Camera cam;
    private Vector3 objPos, objSpeed;
    private Vector3 camLookAt, camSpeed;
    private Vector3 realOffset, offsetSpeed;
    private Vector2 realDirections, directionSpeed;
    private float realRotation, rotationSpeed;
    private float realDistance, distanceSpeed;

    // Start is called before the first frame update
    void Start()
    {
        cam = GetComponent<Camera>();

        objPos = targetObject.transform.position;

        camLookAt = objPos;
        camSpeed = new Vector3(0.0f, 0.0f, 0.0f);
        realDirections = targetDirections;
        directionSpeed = new Vector2(0, 0);
        realRotation = targetRotation;
        rotationSpeed = 0;
        realDistance = targetDistance;
        distanceSpeed = 0;
    }

    // Update is called once per frame
    void Update()
    {
        UpdateObjInfo();
        UpdateCamera();
    }

    private void UpdateObjInfo()
    {
        objSpeed = (targetObject.transform.position - objPos) / Time.deltaTime;
        objPos = targetObject.transform.position;
    }

    private void UpdateCamera()
    {
        if (!forceSync)
        {
            {
                Vector3 accel = L0Weight * (objPos - camLookAt) + L1Weight * (objSpeed - camSpeed);
                Vector3 dv = accel * Time.deltaTime;
                camLookAt += (camSpeed + dv * 0.5f) * Time.deltaTime;
                camSpeed += dv;
            }

            {
                Vector3 accel = L0Weight * (targetOffset - realOffset) + L1Weight * (-offsetSpeed);
                Vector3 dv = accel * Time.deltaTime;
                realOffset += (offsetSpeed + dv * 0.5f) * Time.deltaTime;
                offsetSpeed += dv;
            }

            {
                Vector2 accel = L0Weight * (targetDirections - realDirections) + L1Weight * (-directionSpeed);
                Vector2 dv = accel * Time.deltaTime;
                realDirections += (directionSpeed + dv * 0.5f) * Time.deltaTime;
                directionSpeed += dv;
            }

            {
                float accel = L0Weight * (targetRotation - realRotation) + L1Weight * (-rotationSpeed);
                float dv = accel * Time.deltaTime;
                realRotation += (rotationSpeed + dv * 0.5f) * Time.deltaTime;
                rotationSpeed += dv;
            }

            {
                float accel = L0Weight * (targetDistance - realDistance) + L1Weight * (-distanceSpeed);
                float dv = accel * Time.deltaTime;
                realDistance += (distanceSpeed + dv * 0.5f) * Time.deltaTime;
                distanceSpeed += dv;
            }
        }
        else
        {
            camLookAt = targetObject.transform.position;
            realOffset = targetOffset;
            realDirections = targetDirections;
            realRotation = targetRotation;
            realDistance = targetDistance;
            camSpeed = Vector3.zero;
            objSpeed = Vector3.zero;
            offsetSpeed = Vector3.zero;
            distanceSpeed = 0;
            rotationSpeed = 0;
            directionSpeed = Vector2.zero;
        }

        Vector3 objCoord = sphereCoordToObjectCoord();
        cam.transform.position = camLookAt + objCoord + realOffset;
        cam.transform.LookAt(camLookAt + realOffset, realRotationsToWorldUp(-objCoord.normalized));
    }

    private Vector3 sphereCoordToObjectCoord()
    {
        float phi = Mathf.Deg2Rad * realDirections[0], theta = Mathf.Deg2Rad * realDirections[1];
        return new Vector3(
            Mathf.Cos(theta) * Mathf.Cos(phi),
            Mathf.Sin(theta),
            Mathf.Cos(theta) * Mathf.Sin(phi)
        ) * realDistance;
    }

    private Vector3 realRotationsToWorldUp(Vector3 lookdir)
    {
        float phi = Mathf.Deg2Rad * realDirections[0];
        float rot = Mathf.Deg2Rad * realRotation;

        // distance to divider (90 or 270 deg), this is an periodical functioin in [0, 360]
        // the sign of this var shows whether theta is in range of (90, 270)
        float diff = (90 - Mathf.Abs(realDirections[1] - 360 * Mathf.Floor(realDirections[1] / 360) - 180)) / 90;

        // rotation coords, x axis is the original up direction without rotation
        Vector3 rotX, rotY;
        if (Mathf.Abs(lookdir.y) < 1.0f)
        {
            // up is determined by whether the camera cross the divider
            Vector3 up = new Vector3(0, -Mathf.Sign(diff), 0);
            rotY = Vector3.Cross(lookdir, up);
            rotX = Vector3.Cross(rotY, lookdir);
        }
        else
        {
            // special case when y is not suitable for computing up
            rotX = new Vector3(-Mathf.Cos(phi), 0, -Mathf.Sin(phi)); // + 180 deg
            rotY = new Vector3(Mathf.Sin(phi), 0, -Mathf.Cos(phi)); // - 90 deg
        }

        return rotX * Mathf.Cos(rot) + rotY * Mathf.Sin(rot);
    }

}
